Analyse : Concevez une application au service de la santé publique

  • Projet-3 : Analyse des données
  • IDEE APPLICATION
  • 1. Chargement du jeu de données (dataset en anglais)
  • Analyse des dates de création et modification des produits
  • ANALYSE UNIVARIEE
    • Quantitatives continues
    • Variable quantitative discrete : Le nutriscore est-il bien répartit ?
      • 'nutrition-score-fr_100g'
    • Les variables qualitalives
      • Variables qualitatives nominales
      • Qui sont les sources de ces données ?
      • Les marques sont_elles disponibles sur le marché français ?
      • Catégories
      • Les additifs à surveillées sont ils des informations dont nous pourrions disposer ?
        • Additifs ciblés
    • Variables qualitatives ordinales
      • Répartition des nutrition_grades et l'application
  • ANALYSE BIVARIEES
    • Les corrélations
    • Fonctions de test pour les analyses
      • Tester la normalité
    • Nutriscore | Nutrigrade
    • Nutrigrade | Protéines
    • Nutrigrade | Sodium
  • ANALYSE MULTIVARIEE
    • Regression linéaire multiple
    • Analyse en Composantes Principales (ACP)
    • Clustering
  • BILAN

Projet-3 : Analyse des données¶

In [1]:
import pandas as pd
import numpy as np
import datetime as dt
import sys
import warnings
import IPython as ip
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import scipy.stats as st
from scipy.stats import t, shapiro
from scipy.stats import normaltest
import statsmodels 
import statsmodels.api as sm 
import statsmodels.formula.api as smf
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.graphics.gofplots import qqplot
# ACP
from sklearn.preprocessing import StandardScaler
from sklearn import decomposition
from sklearn.decomposition import PCA
from sklearn import decomposition
from sklearn import preprocessing


from IPython.display import display
import missingno as msno 
# Configuration pour travail avec fichier python "tools" de fonctions
%load_ext autoreload
%aimport tools
# Recharger les modules pour la conception des fichiers tools
%autoreload 1

# Set option
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)


warnings.filterwarnings("ignore")

IDEE APPLICATION¶


  • L'application doit permettre aux personnes qui recherchent des conseils alimentaires en cas de maladie rénale chronique de trouver des informations suppleméntaires (info visuel) qui est spécifique au gens qui doivent préserver au mieux leurs reins (problème rénal).
  • L'application ne remplace pas un régime spécifique fait par un professionnel de santé mais constitue une aide à a décision par une information simplifiée concernant les indicateur qui sont importants de surveiller lorsqu'on doit surveiller son alimentation pour préserver ses reins.

  • Objectifs : COMMENT PROTEGER MES REINS ?
    • Limiter l’apport en sel
    • Contrôler les apports en protéines,
    • Couvrir les besoins nutritionnels,
    • Conserver le plaisir de manger.

On surveille :

  • Le poids : alimentation saine, équilibrée et plaisir :
    • 'nutrition-score-fr_100g','energy_100g'
    • 'fat_100g', 'saturated-fat_100g','carbohydrates_100g', 'sugars_100g', 'fiber_100g'
  • Les indicateurs disponibles :
    • Sel('salt_100g')
    • Sodium('sodium_100g') : sel minéral qui intervient dans l’équilibre hydrique du corps
      • 1g de sel (NaCl) équivaut à 400 mg de sodium (Na)
      • 1 gramme de sodium(Na) correspond donc à 2.5 g de sel
    • Proteines ('proteins_100g') : Au stade d’insuffisance rénale les besoins en protéines sont de 0,8g/Kg/j
      • en fonction du poids :
        • -60kg => 40g
        • 60kg => 46g
        • 70kg => 56g
        • 80kg => 64g
    • Le potassium est un minéral important présent dans un grand nombre d’aliments indispensables au bon fonctionnement des muscles et du coeur. : trop de données manquantes
    • Le phosphore est surtout présent dans les aliments, lié aux protéines.
      • La limitation protéique entraîne déjà une diminution des apports en phosphore.
    • Limiter les additifs :
      • E 338 Acide phosphorique (boisson au cola)
      • E 339 Phosphates de sodium
      • E 340 Phosphates de potassium
      • E 341 Phosphates de calcium
      • E 343 Phosphates de magnésium
      • E 450 Diphosphates
      • E 451 Triphosphates
      • E 452 Polyphosphates

Problématique : Les données du jeu de données peuvent-elles répondre aux objectifs ?


1. Chargement du jeu de données (dataset en anglais)¶

In [2]:
# Import données
data = pd.read_csv('assets/datas/df_app_knnImputer.csv', sep='\t',parse_dates=[2,3], low_memory=False)
In [3]:
df = data.copy()
# Visualisation d'un échantillon de la population
df.sample(5)
Out[3]:
code creator created_datetime last_modified_datetime product_name brands categories_fr countries_fr additives_n additives_fr ingredients_from_palm_oil_n nutrition_grade_fr main_category_fr energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g nutrition_score_fr_100g
27358 0035826100696 usda-ndb-import 2017-03-09 15:42:55 2017-03-09 15:42:55 Trail Mix, Peanuts, Pretzels, Sesame Sticks, C... Food Lion, Food Town Stores Inc. inconnu États-Unis 1.0 E375 - Acide nicotinique 0.0 d inconnu 2092.0 30.00 5.00 43.33 3.33 6.7 16.67 1.77800 0.700 12.0
118239 0726984190543 usda-ndb-import 2017-03-09 13:21:37 2017-03-09 13:21:37 Marionberry Preserves Haggen inconnu États-Unis 2.0 E440 - Pectines,E330 - Acide citrique 0.0 0 inconnu 1255.0 0.00 0.00 70.00 50.00 5.0 0.00 0.00000 0.000 11.0
16783 0021908503165 usda-ndb-import 2017-03-09 14:32:44 2017-03-09 14:32:44 Premium Organic Country Style Potatoes Cascadian Farm Organic, Small Planet Foods Inc. inconnu États-Unis 0.0 0.0 a inconnu 247.0 0.00 0.00 14.12 1.18 1.2 1.18 0.04572 0.018 -1.0
35762 0041250664741 usda-ndb-import 2017-03-09 11:07:12 2017-03-09 11:07:12 Hardwood Smoked Thick Sliced Bacon Meijer inconnu États-Unis 3.0 E339iii - Phosphate de sodium tribasique,E316 ... 0.0 0 inconnu 2301.0 45.00 17.50 0.00 12.40 4.4 35.00 5.20700 2.050 24.2
5989 0011161032175 usda-ndb-import 2017-03-09 20:11:49 2017-03-09 20:11:49 Wieners Shur Fine inconnu États-Unis 4.0 E325 - Lactate de sodium,E339 - Orthophosphate... 0.0 e inconnu 1393.0 28.89 11.11 2.22 2.22 0.0 13.33 1.75006 0.689 21.0
In [4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 240304 entries, 0 to 240303
Data columns (total 23 columns):
 #   Column                       Non-Null Count   Dtype         
---  ------                       --------------   -----         
 0   code                         240304 non-null  object        
 1   creator                      240304 non-null  object        
 2   created_datetime             240304 non-null  datetime64[ns]
 3   last_modified_datetime       240304 non-null  datetime64[ns]
 4   product_name                 240304 non-null  object        
 5   brands                       240304 non-null  object        
 6   categories_fr                240304 non-null  object        
 7   countries_fr                 240304 non-null  object        
 8   additives_n                  240304 non-null  float64       
 9   additives_fr                 240304 non-null  object        
 10  ingredients_from_palm_oil_n  240304 non-null  float64       
 11  nutrition_grade_fr           240304 non-null  object        
 12  main_category_fr             240304 non-null  object        
 13  energy_100g                  240304 non-null  float64       
 14  fat_100g                     240304 non-null  float64       
 15  saturated_fat_100g           240304 non-null  float64       
 16  carbohydrates_100g           240304 non-null  float64       
 17  sugars_100g                  240304 non-null  float64       
 18  fiber_100g                   240304 non-null  float64       
 19  proteins_100g                240304 non-null  float64       
 20  salt_100g                    240304 non-null  float64       
 21  sodium_100g                  240304 non-null  float64       
 22  nutrition_score_fr_100g      240304 non-null  float64       
dtypes: datetime64[ns](2), float64(12), object(9)
memory usage: 42.2+ MB
In [5]:
tools.get_description_variables(df,type_var='categ')
Out[5]:
count unique top freq first last
code 240304 240304 0000000004530 1 NaT NaT
creator 240304 2477 usda-ndb-import 153975 NaT NaT
created_datetime 240304 126364 2017-03-09 10:37:09 19 2012-01-31 14:43:58 2017-04-20 21:13:06
last_modified_datetime 240304 119721 2015-08-09 17:35:48 22 2012-04-08 08:12:35 2017-04-21 00:53:41
product_name 240304 186889 Extra Virgin Olive Oil 192 NaT NaT
brands 240304 46265 inconnue 3145 NaT NaT
categories_fr 240304 16277 inconnu 178919 NaT NaT
countries_fr 240304 81 États-Unis 155671 NaT NaT
additives_fr 240304 38480 102639 NaT NaT
nutrition_grade_fr 240304 6 d 57653 NaT NaT
main_category_fr 240304 2322 inconnu 178919 NaT NaT
In [6]:
tools.get_description_variables(df,type_var='num')
Out[6]:
count mean std min 25% 50% 75% max
additives_n 240304.0 1.787436 2.464454 0.0 0.00000 1.00000 3.00000 31.000000
ingredients_from_palm_oil_n 240304.0 0.017582 0.133063 0.0 0.00000 0.00000 0.00000 2.000000
energy_100g 240304.0 1123.056095 786.012530 0.0 389.00000 1100.00000 1674.00000 3776.000000
fat_100g 240304.0 12.054291 16.700297 0.0 0.00000 4.60000 19.00000 100.000000
saturated_fat_100g 240304.0 4.614094 7.549919 0.0 0.00000 1.25000 6.67000 100.000000
carbohydrates_100g 240304.0 31.203502 28.476440 0.0 6.45000 20.00000 56.76000 100.000000
sugars_100g 240304.0 15.283422 20.688020 0.0 1.01000 5.10000 22.58000 100.000000
fiber_100g 240304.0 2.435193 4.211174 0.0 0.00000 1.02000 3.30000 100.000000
proteins_100g 240304.0 7.113039 8.106444 0.0 0.71000 4.88000 10.00000 100.000000
salt_100g 240304.0 1.592437 6.193162 0.0 0.06858 0.59182 1.37414 100.000000
sodium_100g 240304.0 0.626730 2.436071 0.0 0.02700 0.23300 0.54300 39.370079
nutrition_score_fr_100g 240304.0 8.959197 8.787331 -15.0 1.00000 9.00000 15.80000 40.000000

Analyse des dates de création et modification des produits¶

In [7]:
add_per_year = df['code'].groupby(by=df['created_datetime'].dt.year).nunique()
modified_per_year = df['code'].groupby(by=df['last_modified_datetime'].dt.year).nunique()

fig=plt.figure(figsize=(12,8))

font_title = {'family': 'serif',
              'color':  '#114b98',
              'weight': 'bold',
              'size': 18,
             }

sns.set_style("whitegrid")
plt.plot(add_per_year, 
         color="#114b98", 
         label="Ajouts")
plt.plot(modified_per_year, 
         color="#00afe6", 
         label="Modifications")
plt.title("Evolution des créations et modifications de produits par année", 
          fontdict=font_title)
plt.xlabel("Année")
plt.ylabel("Nombre de produits")
plt.legend()
plt.savefig("assets/graphiques/Evolutions_dates.jpg")

plt.show()

Bilan date : Le pic de 2016, le début d'une collecte massive

  • Quelle que soit les valeurs faisant référence aux dates : on observe un augmentation significative dans les années 2015-2017
  • created_t = created_datetime
  • last_modified-t = last_modified_datetime
  • created_t dispose d'information remontant à 1970 : 01/01/1970 est une erreur car c'est la seule et la date est antérieur au projet
  • Explication des dates :
    • Le Nutri-Score est prévu dans la loi de 2016 en France
    • Mis en place en France en 2017, l'étiquetage nutritionnel Nutri-Score s'applique aujourd'hui dans sept pays

ANALYSE UNIVARIEE¶


In [8]:
float_columns  = df.select_dtypes(include=['float64']).columns.to_list()
object_columns = df.select_dtypes(include=['object']).columns.to_list()
datetime_columns = df.select_dtypes(include=['datetime64[ns]']).columns.to_list()
In [9]:
# On écarte les dates
# Variables numériques
cols_num = df.select_dtypes(include=[np.number]).columns.to_list()
# Variables quantitatives discrètes
cols_quant_discr = ['additives_n','ingredients_from_palm_oil_n','nutriscore_score_fr']
# Variables quantitatives continue
cols_quant_cont = ['energy_100g', 'fat_100g','saturated_fat_100g', 
                   'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
                   'proteins_100g', 'salt_100g', 'sodium_100g']

Quantitatives continues¶

In [10]:
# Mesures de tendances centrales des colonnes quantitatives continues
tools.stat_descriptives(df,cols_quant_cont)
Out[10]:
Desc energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g
mean 1123.056095 12.054291 4.614094 31.203502 15.283422 2.435193 7.113039 1.592437 0.626730
median 1100.000000 4.600000 1.250000 20.000000 5.100000 1.020000 4.880000 0.591820 0.233000
var 617813.126622 278.898761 57.001032 810.904287 427.992372 17.733913 65.714153 38.355100 5.934415
std 786.010895 16.700262 7.549903 28.476381 20.687977 4.211165 8.106427 6.193149 2.436065
skew 0.425871 2.231646 3.502017 0.623817 1.725949 5.409043 2.126997 11.073758 11.093051
kurtosis -0.446573 6.514148 22.325536 -0.954933 2.479089 58.608099 8.571377 142.319727 142.787027
mode 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0 0 0.0
Min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
Max 3776.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 100.000000 39.370079

Mediane et moyenne sont écartées

  • Skewness : coefficient d'asymetrie (mesure de l'asymétrie)
    • energy_100g et carbohydrate_100g semble se rapprocher d'une distribution symétrique (skew proche de 0)
    • sugars_100g fat_100g saturated_fat_100g fiber_100g proteins_100g salt_100g sodium_100g (skew > 0)

  • kurtosis : coefficient d'applatissement
    • Un kurtosis élevé saturated_fat_100g,fiber_100g,salt_100g,sodium_100g , indique que la distribution est plutôt pointue. kurtisis > 3 on parle de distribution leptokurtique
    • À l'opposé, un kurtosis proche de zéro indique une distribution relativement aplatie pour une même variance. kurtosis < 3, on parlera de distribution platikurtique pour energy_100g, carbohydrates_100g.
    • Si kurtosis = 3, sugars_100g de distribution mesokurtique (kurtosis de la forme normale)

In [11]:
def var_hist(var, i):
    subset = df[var]
    n_df_valide = len(df)
    xbar = np.mean(df[var]) # Moyenne
    sprime = np.std(df[var], ddof=1) # Ecart-type
    sprime2 = np.var(df[var], ddof=1) #Variance non biaisée
    
    ax = fig.add_subplot(i)
    ax.hist(subset, density=True)
    ax.axvline(xbar, color='r', linewidth=2, label="Moyenne empirique")
    bins = np.arange(df[var].min(),df[var].max(),0.05)
    y = st.norm.pdf(bins, xbar, sprime)
    ax.plot(bins, y, '--', label="Densité normale")
    ax.legend()
    ax.set_xlabel(var, fontsize=12)
    ax.set_ylabel('Densité', fontsize=12)
    ax.set_title('Distribution de '+str(var), fontsize=18)
In [12]:
liste_var = cols_quant_cont
plt.style.use('seaborn-whitegrid')
fig = plt.figure(figsize=(20,30),constrained_layout=False)
i = 331
for var in liste_var :
    var_hist(var, i)
    i+=1
plt.savefig("assets/graphiques/analyse univariee histo_dfComplet.jpg")
In [13]:
# Representation graphique des outliers:
a = 3  # nombre de lignes
b = 3  # nombre de colonnes
c = 1  # initialisation
fig = plt.figure(figsize=(20,8))

for i in df.loc[:, cols_quant_cont]: # pour toute les colonnnes quantatives
    plt.subplot(a, b, c) # maillage des subplot
    plt.title('{} (boxplot)'.format(i, a, b, c))# titres des box plot
    plt.xlabel(i) # xlabel = nom de la colonne
    sns.boxplot(x = df[i]) # faire un boxplot sns
    c = c + 1 # incrementation ==> création d'un nouveau box plot
plt.subplots_adjust(left=0.125, # gerer les espacements
                    bottom=0.1, 
                    right=0.9, 
                    top=0.9, 
                    wspace=0.2, 
                    hspace=0.35)

Bilan :

  • Toutes les variables quantitatives continues sont asymétriques avec un skewness montrant un étalement vers la droite
  • Un pic de valeurs autour de 0 pour toutes les variables (concernent des produits qui ne contiennent pas ces éléments en effet un produit peut ne pas contenir de sel... ce ne sont pas forcement des erreurs)
  • Si l'on except le sucre aucun des Kurtosis de nos variable quantitatives continues n'est en adéquation avec une distribution qui suit la forme normale
  • Les produits présents sont plutôt des produits caloriques, gras et sucrés.

Variable quantitative discrete : Le nutriscore est-il bien répartit ?¶

'nutrition-score-fr_100g'¶

In [14]:
# On s'occupe ici uniquement des nutrigrades complétés
df_nutri = df[~(df['nutrition_grade_fr']=='0')]
# On s'occupe ici uniquement des nutrigrades complétés
df_nutriscore = df[~(df['nutrition_score_fr_100g']=='0')]
df_nutriscore = df_nutriscore[~(df_nutriscore['nutrition_grade_fr']=='0')] 
In [15]:
# Courbe de distribution du nutriscore
plt.figure(figsize=(12, 8))

sns.histplot(df_nutriscore['nutrition_score_fr_100g'], kde=True,
             color='SteelBlue', label='Nutri_score pour 100g de produit')
plt.title("Distribution du nutri-score", fontsize=14)
plt.xlim(-15, 40)
plt.xlabel('Score', fontsize=12)
plt.ylabel('Nombre de produits par score', fontsize=12)
plt.legend()
plt.show()
In [16]:
fig = plt.figure(figsize=(15, 6))

ax1 = fig.add_subplot(1, 2, 1)
box = sns.boxplot(data=df_nutri['nutrition_score_fr_100g'], color='SteelBlue', ax=ax1)
# box.set(ylabel=unite)

plt.grid(False)

ax2 = fig.add_subplot(1, 2, 2)
ax2 = sm.qqplot(df_nutri['nutrition_score_fr_100g'],
             line='r', ax=ax2)
plt.grid(False)

fig.suptitle('Dispersion des nutrition-score-fr_100g', fontweight='bold', size=14)
plt.show()
In [17]:
col = ['nutrition_score_fr_100g']
tools.stat_descriptives(df_nutri,col)
Out[17]:
Desc nutrition_score_fr_100g
mean 9.131191
median 10.000000
var 81.995724
std 9.055149
skew 0.116733
kurtosis -1.017174
mode 0 0.0
Min -15.000000
Max 40.000000
In [18]:
# définition des bacs
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.IntervalIndex.from_tuples.html
liste_bins = pd.IntervalIndex.from_tuples(
    [(-15, -1), (0, 2), (3, 10), (10, 18), (19, 40)])
tools.distribution_variables_plages_perc_donnees(df_nutri,'nutrition_score_fr_100g',liste_bins)
Out[18]:
Plage nb_données %_données
(-15, -1] 33037 16.241980
(0, 2] 20121 9.892087
(3, 10] 35405 17.406160
(10, 18] 58204 28.614832
(19, 40] 32121 15.791647

Bilan nutriscore


  • La distribution semble non normale surtout pour les produits de tête et de fin.
  • La moyenne et la médiane sont autour d'un score de 10.
  • L'amplitude est de -15 à 40 donc tous les nutri-scores sont représentés.
  • La majorité des produits d'Open Food Facts nutri-score > 10

Les variables qualitalives¶

In [19]:
# On visualise le nombre de valeurs uniques contenu dans les colonnes de type object
for col in df.select_dtypes('object'):
    print(f'{col:-<50} {df[col].nunique()}')
code---------------------------------------------- 240304
creator------------------------------------------- 2477
product_name-------------------------------------- 186889
brands-------------------------------------------- 46265
categories_fr------------------------------------- 16277
countries_fr-------------------------------------- 81
additives_fr-------------------------------------- 38480
nutrition_grade_fr-------------------------------- 6
main_category_fr---------------------------------- 2322

Variables qualitatives nominales¶

In [20]:
# Variables qualitatives ou modalités
# Variables qualitatives nominales
cols_qual_nom = ['code','creator','product_name','brands', 
                 'categories_fr','main_category_fr', 'countries_fr','additives_fr']
In [21]:
def top_N_pie (df,var,name,n,taille,perc) : 
    ''' 
    Fonction qui visualise les n plus grand d'une colonne avec ou sans pourcentage
    parametres : 
        df
        var : colonne ciblée
        name : 'nom de la colonne '
        n : nombre de top voulu
        taille : taille du pieplot
        per : si True : affiche les pourcentages
    '''
    target = df.groupby(by=var)['code'].nunique().sort_values(ascending=False)
    # Graphiques top N
    
    fig, ax = plt.subplots(figsize=(taille, taille), subplot_kw=dict(aspect="equal"))
    explodes = np.zeros(n)
    explodes[0] = .1
    # calcul des pourcentages
    if perc: 
        def pct_tot(pct):
            tot = round(pct*target[:n].sum(),0)
            tot_pct = tot/target.sum()
            return "{:.1f}%\n({:.0f})".format(tot_pct,(tot/100))
        plt.pie(target[:n], labels=target[:n].index,
            startangle=45,
            shadow=True,
            autopct=lambda pct: pct_tot(pct),
            explode=explodes,
            textprops=dict(color="black",size=12, weight="bold"))
    else : 
        plt.pie(target[:n], labels=target[:n].index,
                startangle=45,
                shadow=True,
                explode=explodes,
                textprops=dict(color="black",size=10, weight="bold"))
    plt.title(f"TOP {n} : {name}",fontweight='bold',fontsize=24)
    plt.show()

Qui sont les sources de ces données ?¶

In [22]:
# Nombre de créateurs, sources des données
print(f"Nombre de sources unique : {df['creator'].nunique()}")
Nombre de sources unique : 2477
In [23]:
top_N_pie(df,'creator','Contributeurs',5,12,True)
plt.savefig("assets/graphiques/Top_Contributeurs.jpg")
<Figure size 640x480 with 0 Axes>
  • 2478 contributeurs dont des institutions internationales mais aussi des particuliers
  • La question de la fiabilité des données se pose dès les 6 plus gros contributeurs
    • La majorité des données provient de l'USDA : Misitère de l'agriculture américain : https://www.usda.gov/topics/trade/importing-goods
    • openfoodfacts-contributors : Communauté de contributeurs : https://world.openfoodfacts.org/contribute
    • kiliweb : ??
    • openfood-ch-import : sûrement une instition suisse mais pas de certitudes
    • date-limie-app : ??
    • Tacite : contributeur pernonnel ?

Les marques sont_elles disponibles sur le marché français ?¶

  • Trouve t-on dans le jeu de données les marques qui sont présentent sur le marché français, consommateurs cible de notre application ?
In [24]:
df['brands'].nunique()
Out[24]:
46265
In [25]:
# On s'occupe ici uniquement des catégories renseignées
df_brands = df[~(df['brands']=='inconnue')]
df_brands.shape
Out[25]:
(237159, 23)
In [26]:
# Tableau fréquences
dico = df_brands.groupby('brands')['brands'].count().sort_values(ascending=False).to_dict()
nom = 'brands'
col1 = 'Nom_' + nom
col2 = 'Nbr_' + nom
col3 = 'Fréquence (%)'
df_gpe = pd.DataFrame(dico.items(), columns=[col1, col2])
df_gpe[col3] = (df_gpe[col2] * 100) / len(df_brands)
df_gpe.head(10)
Out[26]:
Nom_brands Nbr_brands Fréquence (%)
0 Carrefour 2362 0.995956
1 Auchan 1768 0.745491
2 Meijer 1702 0.717662
3 U 1681 0.708807
4 Kroger 1454 0.613091
5 Leader Price 1372 0.578515
6 Casino 1233 0.519904
7 Ahold 1181 0.497978
8 Roundy's 1112 0.468884
9 Spartan 1063 0.448223
In [27]:
# Wordecloud
from wordcloud import WordCloud

wordcloud = WordCloud(width=800,height=400, background_color="white",max_words=100).generate_from_frequencies(dico)
plt.figure(figsize=(12, 10))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
  • Sur 46265 marques différentes dans le df
    • 237159 produit non renseignés : 'inconnues'
  • Les marques disponibles en France sont bien représentées :
  • Carrefour, Auchan, Leader Price, U, Casino, Monoprix, Picard, Bjorg, Nestlé, Lipton, Cora, Harris... sont des marques populaires en France
In [28]:
 df_gp_red = df_gpe.head(10)
sns.set_style("whitegrid")
plt.figure(figsize=(8, 4))
sns.barplot(
    y=df_gp_red[col1],
    x=df_gp_red[col3],
    data=df_gp_red,
    color='SteelBlue')
plt.title('Répartition de la présence des marques dans le jeu de données')
plt.grid(False)
plt.tight_layout()
plt.show()

Catégories¶

In [29]:
df['categories_fr'].nunique()
Out[29]:
16277
In [30]:
# On s'occupe ici uniquement des catégories renseignées
df_categ = df[~(df['categories_fr']=='inconnu')]
In [31]:
tools.affiche_wordcloud_tabfreq(df_categ,'categories_fr','categories',)
Nom_categories Nbr_categories Fréquence (%)
Snacks sucrés,Biscuits et gâteaux,Biscuits 691 1.125682
Snacks sucrés,Chocolats,Chocolats noirs 529 0.861774
Aliments et boissons à base de végétaux,Aliments d'origine végétale,Petit-déjeuners,Céréales et pommes de terre,Céréales et dérivés,Céréales pour petit-déjeuner 450 0.733078
Snacks sucrés,Biscuits et gâteaux,Biscuits,Biscuits au chocolat 409 0.666287
Snacks salés,Apéritif,Biscuits apéritifs 404 0.658141
Snacks sucrés,Confiseries,Bonbons 374 0.609269
Snacks sucrés,Chocolats,Chocolats au lait 363 0.591350
Produits laitiers,Yaourts 349 0.568543
Snacks sucrés,Chocolats 301 0.490348
Epicerie,Sauces 299 0.487090
In [32]:
tools.affiche_wordcloud_tabfreq(df_categ,'main_category_fr','Main categories',affword=False)
Nom_Main categories Nbr_Main categories Fréquence (%)
Boissons 2290 3.730553
Epicerie 2278 3.711004
Aliments et boissons à base de végétaux 2274 3.704488
Chocolats 2252 3.668649
Conserves 2014 3.280932
Biscuits 1853 3.018653
Plats préparés 1813 2.953490
Surgelés 1713 2.790584
Petit-déjeuners 1566 2.551112
Snacks sucrés 1515 2.468030
  • Parmi les catégories les plus représentés ont retrouve beaucoup de produits considérés comme a surveiller dans une alimentation saine.
  • A défaut d'une sur représentation des produits sain on peut exploiter ces informationspour informer le consommateur sur les produits à surveiller. Une bonne alimentation passe aussi par le plaisir et ne doit pas être stigmatisé sans avis médical personnalisé contraire.

  • Pour notre appli cette source de données est importantes et l'application devra signaler les choses de manière pédagogique

    • Alimentation : interdite par le medecin
    • alimentation : apport à surveiller
    • alimentation : recommandé

Les additifs à surveillées sont ils des informations dont nous pourrions disposer ?¶


Additifs ciblés¶


  • Limiter les additifs :
     - E 338 Acide phosphorique (boisson au cola)
     - E 339 Phosphates de sodium
     - E 340 Phosphates de potassium
     - E 341 Phosphates de calcium
     - E 343 Phosphates de magnésium
     - E 450 Diphosphates
     - E 451 Triphosphates
     - E 452 Polyphosphates
In [33]:
# On s'occupe ici uniquement des catégories renseignées
df_additives = df[~(df['additives_fr']==' ')]
In [34]:
df_additives_target = df_additives.copy()
df_additives_target = df_additives_target[df_additives_target['additives_fr'].str.contains("338|339|340|341|343|450|451|452")]
In [35]:
tools.affiche_wordcloud_tabfreq(df_additives_target,'additives_fr','Additives',affword=False)
Nom_Additives Nbr_Additives Fréquence (%)
E452vi - Tripolyphosphate de sodium et de potassium 345 1.564271
E339iii - Phosphate de sodium tribasique,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 290 1.314895
E339iii - Phosphate de sodium tribasique 286 1.296758
E339 - Orthophosphates de sodium 211 0.956699
E339 - Orthophosphates de sodium,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 194 0.879619
E450 - Sels métalliques de diphosphates 178 0.807073
E325 - Lactate de sodium,E339 - Orthophosphates de sodium,E262ii,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 177 0.802539
E325 - Lactate de sodium,E339iii - Phosphate de sodium tribasique,E262ii,E316 - Erythorbate de sodium,E250 - Nitrite de sodium 175 0.793471
E375 - Acide nicotinique,E101 - Riboflavine,E450 - Sels métalliques de diphosphates 159 0.720925
E341iii - Phosphate de tricalcium 132 0.598504
Les addfitifs que nous ciblons sont dans la base de données
  • Les addfitifs que nous ciblons sont dans la base de données

Variables qualitatives ordinales¶

In [36]:
# Variables qualitatives ordinales
cols_qual_ord = ['nutriscore_grade_fr']

Répartition des nutrition_grades et l'application¶

  • Comment exploiter les nutrigrades pour notre application ?
In [37]:
# On s'occupe ici uniquement des nutrigrades complétés
df_nutri = df[~(df['nutrition_grade_fr']=='0')]
In [38]:
nutrition_grade = df_nutri.groupby(by='nutrition_grade_fr')['code'].nunique().sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(aspect="equal"))
explodes = np.zeros(5)
explodes[0] = .1

plt.pie(nutrition_grade, labels=nutrition_grade.index, 
        startangle=0, 
        colors=['#ee8100','#fecb02','#e63e11','#038141','#85bb2f'],
        shadow=True,
        explode=explodes,
        autopct='%1.1f%%',
        textprops=dict(color="black",size=12, weight="bold"))
plt.title("Répartition des Nutrition_grade", fontdict=font_title)
plt.savefig("assets/graphiques/Répartion_nutrigrdes.jpg")
plt.show()

Tous les nutrigrades sont représentés avec beaucoup de produits appartenant aus groupes 'd'et 'e' représentant 47% des produits de la base

Définition : que signifie le Nutri-Score ?

Conçu dans le cadre du Programme National Nutrition Santé, le Nutri-Score est une échelle graphique qui classe de A à E les produits alimentaires en fonction de leurs qualités nutritionnelles. Le système retenu se base ainsi sur un code à 5 couleurs : du vert pour les produits équilibrés, du rouge pour les aliments trop gras ou trop sucrés et trois couleurs intermédiaires (vert clair, jaune et orange).

► Les aliments classés A sont les plus favorables sur le plan nutritionnel car il s'agit de nutriments et d'aliments à favoriser (fibres, protéines, fruits, légumes, légumineuses, fruits à coques, huile de colza, de noix et d'olive),

► Les aliments classés E ont une moins bonne qualité nutritionnelle car ils contiennent des nutriments à limiter (énergie, acides gras saturés, sucres, sel).

Il s'agit de l'étiquetage nutritionnel officiel recommandé en France. Mis au point par des équipes de recherches internationales, synthétique, compréhensible et fondé sur des bases scientifiques, ce logo fournit une information immédiate au consommateur sur la qualité nutritionnelle des produits qu'il achète afin de l'aider à faire facilement les bons choix dans les rayons des supermarchés. 

ANALYSE BIVARIEES¶

Les corrélations¶

In [39]:
sns.pairplot(df_nutri.sample(frac=0.05), hue="nutrition_grade_fr")
plt.savefig("assets/graphiques/Pairplot_Nutrition grade.jpg")
In [40]:
sns.clustermap(df_nutri.corr(),annot=True)
Out[40]:
<seaborn.matrix.ClusterGrid at 0x17764f7f3a0>

Bilan


  • On observe des correlations entre le nutrigrade et les autres variables interressantes pour l'application
  • Certaines variables sont très corrélées :
    • Cette matrice des corrélations ne nous apporte pas rééllement d'informations mais confirme mathématiquement des éléments logiques : salt_100g est très fortement corrélé avec sodium_100g, fat_100g avec satured-fat_100g... Il faudra cependant tenir compte de ces fortes corrélation dans nos modèles, la colinéarité dégradant les performances.
In [44]:
# Répartition des variables quantitative en fonction du nutrigrade
colors_nutri = ['#038141','#85bb2f','#fecb02','#ee8100','#e63e11']
fig = plt.figure(figsize=(20, 35))

for i, c in enumerate(df_nutriscore.select_dtypes('float'), 1):
    ax = fig.add_subplot(6, 2, i)
    sns.boxplot(data=df_nutriscore, x='nutrition_grade_fr', y=c,order='abcde', ax=ax,palette=colors_nutri)
    plt.grid(False)
    plt.xticks(fontsize=16)
    plt.yticks(fontsize=16)

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.suptitle('Répartition des variables quantitatives en fonction du nutrigrade', fontsize=30)
plt.savefig("assets/graphiques/Répartition des variables quantitatives en fonction du nutrigrade.jpg")
plt.show()
  • Répartition générale :

    • Les données sont généralement asymétriques, la majorité d'entre elles sont situées sur le côté supérieur ou inférieur du graphique et présentent de nombreux outliers. L'asymétrie indique que les données peuvent ne pas être normalement distribuées.
      • Beaucoup de valeurs atypiques quelque soit le groupe
        • la diversité des produits explique ces résultats
  • Energie : Plus un produit apporte de l'énegie plus il faut surveiller les quantités (classes CDE)

  • fat, satured-fat : Plus un produit
    • Centres : AB peu gras, CDE : plus gras, à surveiller
    • Dispersion : difference de dispersion CDE
  • Sucres :
    • Centres : Plus un produit contient de sucre plus son nutrigrade augmente
    • Dispersion : beaucoup d'outliers dans les AB mais l'apport de sucre est limité 80g pourt 100g
  • Protéines :
    • Centres (comparer les médianes) : les médianes sont proches entre les groupes
    • Dispersion : Très marquées par les outliers
  • Sel (sodium et sel): Plus un produit contient de sel plus il est à surveiller
    • Centres (comparer les médianes) : Les produits à éviter et à surveiller font parti de ABC
    • Dispersion : beaucoup de produit très salé appartiennent aux groupes considérés comme à maîtriser : C,D,E
  • Nutri-score : conforme à la séparation nutrigraded (assez normal).

  • On regarde les proteines importantes pour notre application :

    • Hypothèse plus un produit est considéré comme sain (A, B) moins il contient de protéines

Fonctions de test pour les analyses¶

Tester la normalité¶

In [49]:
df_test = df_nutriscore.copy()
# on ajoute une colonne corespondant à la labelEncoder de la variable nutrigrade
le = preprocessing.LabelEncoder()
le.fit(["a", "b", "c", "d","e"])
df_test['nutrition_grade_fr_le'] = le.transform(df_test['nutrition_grade_fr'])
In [50]:
# Test de Shapiro-Wilk
shapiro(df_nutriscore['proteins_100g'])
Out[50]:
ShapiroResult(statistic=0.8269742131233215, pvalue=0.0)

Le test de Shapiro-Wilk est un test de normalité. Il est utilisé pour déterminer si un échantillon provient ou non d'une distribution normale.

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Comme la valeur p est inférieure à 0,05, nous rejetons l'hypothèse nulle. les données de l'échantillon ne proviennent pas d'une distribution normale.


Confirmation avec normaltest basé sur D'Agostino an Pearson's

Hypothèses :

  • H0 : la distribution des données est normale (P>0,05)
  • H1 : la distribution des données n'est pas normale (P<0,05)

Puisque la valeur p est inférieure à 0,05, nous rejetons l'hypothèse nulle. Nous avons suffisamment de preuves pour affirmer que les données de l'échantillon ne proviennent pas d'une distribution normale.

Création d'un fonction pour faire les tests

In [118]:
def test_normalite(df,var):
    ''' Création d'une fonction pour tester la normalioté de la distribution des variables
    '''
    print(f"Variable : {var}")
    stat, p = shapiro(df[var]) 
    print ('Test Shapiro')
    print('stat=%.3f, p=%.3f' % (stat,p))
    #interprétation
    if p > 0.05:
        print("Distribution probablement gaussienne")
    else:
        print("Distribution non Gaussienne")
    print("------------------------------------------")
    print("Test normaltest (d'Agostino)")
    stat, p = normaltest(df[var])
    print('Statistics=%.3f, p=%.3f' % (stat, p))
# interpretation
    alpha = 0.05
    if p < alpha:  # null hypothesis: x comes from a normal distribution
        print("H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) ")
    else:
        print("H0 ne peut être rejetée - la distribution des données suit une loi normale (P>0,05)")
    print("________________________________________________________")
In [ ]:
 

Nutriscore | Nutrigrade¶

In [41]:
# On s'occupe ici uniquement des nutrigrades et nutriscore complétés
df_nutriscore = df[~(df['nutrition_score_fr_100g']=='0')]
df_nutriscore = df_nutriscore[~(df_nutriscore['nutrition_grade_fr']=='0')] 
In [42]:
# graph
sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_score_fr_100g", hue="nutrition_grade_fr")
plt.show()
In [43]:
fig, axes = plt.subplots(1, 2, sharex=False, sharey=False, figsize=(21,8))
fig.suptitle(r"Répartition des scores Nutriscore et de leurs grades" "\n", fontsize=22)

sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_grade_fr", hue="nutrition_grade_fr", ax=axes[0])
axes[0].set_title('Grades de Nutriscores')
axes[0].set_xlabel("nutrition_grade_fr")
axes[0].set_ylabel("Nombre de produits")

sns.histplot(data=df_nutriscore.sort_values("nutrition_grade_fr"), x="nutrition_score_fr_100g", hue="nutrition_grade_fr", ax=axes[1])
axes[1].set_title('Scores de Nutriscores')
axes[1].set_xlabel("Score Nutriscore")
axes[1].set_ylabel("Nombre de produits")

plt.show()
In [143]:
# Préparation des variables de travail pour les graphiques et les tests
gb = df_nutriscore.groupby('nutrition_grade_fr')['nutrition_score_fr_100g']
df_nutriscore_nutrigrade = pd.DataFrame([gb.get_group(n).values for n in list('abcde')],
                             index=list('abcde')).T
In [144]:
tools.stat_descriptives(df_nutriscore_nutrigrade, ['a', 'b', 'c', 'd', 'e'])
Out[144]:
Desc a b c d e
mean -3.426405 0.908934 6.356685 14.059927 21.967796
median -3.000000 1.000000 6.000000 14.000000 22.000000
var 4.623705 0.748122 6.329803 5.342098 10.310623
std 2.150280 0.864940 2.515910 2.311298 3.211016
skew -0.662412 -0.590996 0.039678 -0.089960 -0.117970
kurtosis 0.731734 3.785613 -1.413975 -0.253618 2.004625
mode 0 -1.0 0 0.0 0 3.0 0 14.0 0 20.0
Min -15.000000 -10.000000 2.000000 6.000000 10.000000
Max 17.000000 2.000000 10.000000 18.000000 40.000000
In [130]:
test_normalite(df_test,'nutrition_score_fr_100g')
Variable : nutrition_score_fr_100g
Test Shapiro
stat=0.968, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=51833.900, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
  • Lien nutriscore nutrigrade est siginificatif :
    • les moyennes témoignent du classement :
      • plus le nutrition_score es bas plus sa moyenne est basse et inversement
  • nutrigrade :
    • La répartition du nombre de produits en fonction de leur appartenance a un groupe Grade de Nutriscore :
      • Le groupe de produit d est le plus nombreux
        • Les produits considérés comme a surveillés sont bien représenté (d et e)
      • Les 5 groupes sont présents
    • Concernant les scores entre -15 et 40 :
      • Les scores les plus représentés sont en entre 0 et 5
      • Pic autour des 15 (grade d)
      • Les grade sont bien représentatifs des scores et sont plutôt diversifiés

note : Les variances ne sont pas comparables, la distribution des valeurs ne suit pas une loi normale

In [ ]:
 
In [ ]:
 

Nutrigrade | Protéines¶

  • Question :
    • La quantité de protéine est-elle liée au nutrigrade ?
In [146]:
# Préparation des variables de travail pour les graphiques et les tests
gp = df_nutriscore.groupby('nutrition_grade_fr')['proteins_100g']
df_nutrigrade_protein = pd.DataFrame([gp.get_group(n).values for n in list('abcde')],
                             index=list('abcde')).T
df_nutrigrade_protein = df_nutrigrade_protein.dropna()
df_a = df_nutrigrade_protein['a']
df_b = df_nutrigrade_protein['b']
df_c = df_nutrigrade_protein['c']
df_d = df_nutrigrade_protein['d']
df_e = df_nutrigrade_protein['e']
In [46]:
plt.figure(figsize=[10, 10])
# colors_nutri = ['#038141','#85bb2f','#fecb02','#ee8100','#e63e11']
# Boxplot protéines/nutri-score grade
plt.subplot(2, 1, 1)
sns.boxplot(data=df_nutriscore, x='nutrition_grade_fr', y='proteins_100g',
            palette=colors_nutri, order='abcde')
plt.ylim(0, 100)
plt.ylabel('Nombre de g de protéines pour 100g de produit', fontsize=12)
plt.xlabel('Nutri-grade_fr', fontsize=12)
plt.title('Protéines par nutri-grade', fontsize=14)
plt.grid(False)
# Ajout moyenne des protéines pour tous les produits
moyenne_proteines = df_nutriscore['proteins_100g'].mean()
plt.axhline(y=moyenne_proteines, color='r')

# Violinplot protéines/nutri-score grade
plt.subplot(2, 1, 2)
sns.violinplot(data=df_nutriscore, x='nutrition_grade_fr', y='proteins_100g',
               palette=colors_nutri, order='abcde')
plt.ylabel('Nombre de g de protéines pour 100g de produit', fontsize=12)
plt.xlabel('Nutri-grade', fontsize=12)
plt.grid(False)
# Ajout moyenne des protéines pour tous les produits
plt.axhline(y=moyenne_proteines, color='r')

plt.show()
  • amplitude en 0 et 100g de protéines
  • pas de correlation entre nutrigrade et nombre de protéine pour 100g
In [47]:
# Distplot protéines
sns.distplot(df_nutriscore['proteins_100g'], bins=100, color='SteelBlue')
plt.grid(False)
In [48]:
qqplot(df_nutriscore['proteins_100g'], line='r')
plt.grid(False)
plt.show()
In [125]:
test_normalite(df_nutriscore,'proteins_100g')
Variable : proteins_100g
Test Shapiro
stat=0.827, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=90840.247, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
  • amplitude en 0 et 100g de protéines
  • pas de correlation entre nutrigrade et nombre de protéine pour 100g
  • La distribution ne semble pas suivre une distribution normal
In [53]:
# Statistiques descriptives
tools.stat_descriptives(df_nutriscore, ['proteins_100g'])
Out[53]:
Desc proteins_100g
mean 7.782852
median 5.710000
var 64.586628
std 8.036581
skew 2.011552
kurtosis 7.844765
mode 0 0.0
Min 0.000000
Max 100.000000

On visualise la distribution des protéines

In [54]:
import matplotlib.patches as mpatches

fig = plt.figure(figsize=(8, 6))
label_patches = []

sns.kdeplot(df_nutriscore['proteins_100g'], color='Blue')
label_patch = mpatches.Patch(
    color='Blue',
    label='Ensemble des nutrigrades')
label_patches.append(label_patch)
plt.grid(False)
plt.xlim([-1, 40])

i = 1
for n, c in zip(list('abcde'), colors_nutri):
    i += 1
    sns.kdeplot(df_nutrigrade[n], color=c)
    label_patch = mpatches.Patch(color=c, label=n)
    label_patches.append(label_patch)
    plt.grid(False)
    plt.xlim([-1, 40])

fig.suptitle('Distribution des protéines', fontweight='bold', fontsize=14)
plt.legend(handles=label_patches,bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0., facecolor='white')
plt.tight_layout()
plt.grid(False)
plt.show()
  • L'essentiel des produits entre 0 et 15g de sodium
    • Tous les nutrigrades s'étalent entre 0 et 30
    • beaucoup de produit de la classe b entre 0 et 15 mais a en 0 et 15 ==> à approfondir On visualise la distribution des protéines par nutrigrade pour voir si des tendances peuvent exister
In [96]:
# Histogramme des protéines, général et par nutri-score
def graph_distribution(df,var):
    fig = plt.figure(figsize=(8, 15))

    fig.add_subplot(6, 1, 1)
    sns.distplot(df[var], bins=70, color='SteelBlue')
    plt.grid(False)

    i = 1
    for n, c in zip(list('abcde'), colors_nutri):
        i += 1
        ax = fig.add_subplot(6, 1, i)
        sns.distplot(df_nutrigrade[n], color=c, bins=70)
        plt.grid(False)
    fig.suptitle(f'Histogramme des {var}',
                 fontweight='bold', fontsize=14)

    plt.tight_layout(rect=[0, 0.0, 1, 0.93])
    plt.grid(False)
    plt.show()
In [97]:
graph_distribution(df_nutriscore,'proteins_100g')
  • Aucun nutrigrade ne se distingue au regard de la quantité de proteins pour 100 g ni dans leurs densités
In [147]:
# On test la normalité de la distribution sur les proteines par nutrigrade
for col in df_nutrigrade.columns:
    print(f"Nutrigrade : {col}")
    test_normalite(df_nutrigrade_protein,col)
Nutrigrade : a
Variable : a
Test Shapiro
stat=0.855, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=16449.014, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : b
Variable : b
Test Shapiro
stat=0.722, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=23643.529, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : c
Variable : c
Test Shapiro
stat=0.828, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=14416.667, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : d
Variable : d
Test Shapiro
stat=0.847, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=10600.846, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
Nutrigrade : e
Variable : e
Test Shapiro
stat=0.829, p=0.000
Distribution non Gaussienne
------------------------------------------
Test normaltest (d'Agostino)
Statistics=9263.825, p=0.000
H0 peut être rejetée - H1 : la distribution des données ne suit pas la loi normale (P<0,05) 
________________________________________________________
In [57]:
liste_bins = pd.IntervalIndex.from_tuples(
    [(0, 4), (5, 9), (10, 14), (15, 19), (20, 24), (25, 29),
     (30, 34), (35, 39), (40, 100)])
tools.distribution_variables_plages_perc_donnees(df_nutriscore, 'proteins_100g', liste_bins)
Out[57]:
Plage nb_données %_données
(0, 4] 57852 28.441779
(5, 9] 44923 22.085494
(10, 14] 19937 9.801627
(15, 19] 9812 4.823874
(20, 24] 8187 4.024975
(25, 29] 2984 1.467024
(30, 34] 985 0.484256
(35, 39] 409 0.201077
(40, 100] 850 0.417885
In [58]:
df_nutriscore['proteins_100g'].describe()
Out[58]:
count    203405.000000
mean          7.782852
std           8.036600
min           0.000000
25%           2.000000
50%           5.710000
75%          10.710000
max         100.000000
Name: proteins_100g, dtype: float64
In [148]:
df_nutrigrade_protein.describe()
Out[148]:
a b c d e
count 31373.000000 31373.000000 31373.000000 31373.000000 31373.000000
mean 8.365304 5.109088 7.172807 8.226714 9.567869
std 7.051328 6.608959 7.564990 8.394596 9.348493
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 3.000000 0.620000 1.220000 2.500000 3.450000
50% 7.140000 3.200000 5.400000 6.060000 6.250000
75% 12.280000 7.300000 10.530000 12.390000 13.640000
max 100.000000 100.000000 93.330000 80.000000 100.000000
In [193]:
# Anova OLS
anova_nutrigrade = smf.ols('proteins_100g~nutrition_grade_fr', data=df_nutriscore).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:          proteins_100g   R-squared:                       0.028
Model:                            OLS   Adj. R-squared:                  0.028
Method:                 Least Squares   F-statistic:                     1491.
Date:                Fri, 13 Jan 2023   Prob (F-statistic):               0.00
Time:                        18:31:24   Log-Likelihood:            -7.0958e+05
No. Observations:              203405   AIC:                         1.419e+06
Df Residuals:                  203400   BIC:                         1.419e+06
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   8.3360      0.044    191.083      0.000       8.250       8.421
nutrition_grade_fr[T.b]    -3.2269      0.062    -51.651      0.000      -3.349      -3.104
nutrition_grade_fr[T.c]    -1.3744      0.058    -23.593      0.000      -1.489      -1.260
nutrition_grade_fr[T.d]     0.2438      0.055      4.457      0.000       0.137       0.351
nutrition_grade_fr[T.e]     0.8275      0.059     13.988      0.000       0.712       0.943
==============================================================================
Omnibus:                    91895.107   Durbin-Watson:                   0.937
Prob(Omnibus):                  0.000   Jarque-Bera (JB):           713257.449
Skew:                           2.014   Prob(JB):                         0.00
Kurtosis:                      11.242   Cond. No.                         6.46
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • amplitude en 0 et 100g de protéines
  • pas de correlations flagrantes entre nutrigrade et nombre de protéine pour 100g
  • On rertrouve dans toutes les plages toutes les notes du nutrtion grade
  • 75% des variables contiennent 11g de proteines

  • R² et R² ajusted : SCE/SCT, sont inférieurs à 5% : 0.028 : la variable explicative nutrition_grade_fr n'est pas pertinentes pour expliquer proteins_100g
  • Omnibus : Une valeur proche de zero indique que les données suivent une loi Normal
    • Prob(Obminbus) : Confirme que les données ne suivent pas une loi normale (proche de 1) mais dans notre ici 0.000
  • Caractéristiques de forme
    • Coefficient d'applatissement Kurtosis : 11.242

  • CCL : Rappel de la question : La quantité de protéine est-elle liée au nutrigrade ?
    • Elles est liée mais la quantité de protéine est un des facteurs pris en considération, on ne peut pas distinguer les nutritions grade en fonction de la variable proteins

Nutrigrade | Sodium¶

Question :
    La quantité de sodium est-elle liée au nutrigrade ?
In [64]:
import matplotlib.patches as mpatches

fig = plt.figure(figsize=(8, 6))
label_patches = []

sns.kdeplot(df_nutriscore['sodium_100g'], color='Blue')
label_patch = mpatches.Patch(
    color='Blue',
    label='Ensemble des nutrigrades')
label_patches.append(label_patch)
plt.grid(False)
plt.xlim([-1, 40])

i = 1
for n, c in zip(list('abcde'), colors_nutri):
    i += 1
    sns.kdeplot(df_nutrigrade[n], color=c)
    label_patch = mpatches.Patch(color=c, label=n)
    label_patches.append(label_patch)
    plt.grid(False)
    plt.xlim([-1, 40])

fig.suptitle('Distribution sodium', fontweight='bold', fontsize=14)
plt.legend(handles=label_patches,bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0., facecolor='white')
plt.tight_layout()
plt.grid(False)
plt.show()
In [65]:
df_nutriscore['sodium_100g'].describe()
Out[65]:
count    203405.000000
mean          0.490369
std           1.556126
min           0.000000
25%           0.039370
50%           0.256000
75%           0.536000
max          39.370079
Name: sodium_100g, dtype: float64
In [66]:
liste_bins = pd.IntervalIndex.from_tuples(
    [(0, 0.2), (0.3, 0.5), (0.5, 0.7), (0.8, 1), (1.1, 1.5), (1.6, 2.5),
     (2.6, 3), (3, 4), (4, 40)])
tools.distribution_variables_plages_perc_donnees(df_nutriscore, 'sodium_100g', liste_bins)
Out[66]:
Plage nb_données %_données
(0.0, 0.2] 71320 35.063052
(0.3, 0.5] 37544 18.457757
(0.5, 0.7] 22252 10.939751
(0.8, 1.0] 8500 4.178855
(1.1, 1.5] 6109 3.003368
(1.6, 2.5] 3974 1.953738
(2.6, 3.0] 439 0.215826
(3.0, 4.0] 622 0.305794
(4.0, 40.0] 2248 1.105184
  • La majorité des produit sont entre 0 et 0.2 g de sodium
  • 75% des données ont des valeur < 0.5
In [98]:
graph_distribution(df_nutriscore,'sodium_100g')
In [154]:
# Préparation des variables de travail pour les graphiques et les tests
gb = df_nutriscore.groupby('nutrition_grade_fr')['sodium_100g']
df_nutrigrade_sodium = pd.DataFrame([gb.get_group(n).values for n in list('abcde')],
                             index=list('abcde')).T
In [168]:
df_nutrigrade_sodium.describe()
Out[168]:
a b c d e
count 32971.000000 31373.000000 42096.000000 57653.000000 39312.000000
mean 0.130486 0.207218 0.672157 0.647881 0.592513
std 0.155508 0.279822 2.652204 1.489795 1.185153
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.006000 0.017000 0.050000 0.071000 0.100000
50% 0.051181 0.167000 0.349303 0.366142 0.357000
75% 0.240079 0.365000 0.576000 0.724000 0.740000
max 4.666600 30.708661 39.370079 35.710000 39.370079
  • Plus un produit contient du sel plus son score est elevé et moins il est considéré comme sain
In [192]:
# Anova OLS
anova_nutrigrade = smf.ols('sodium_100g~nutrition_grade_fr', data=df_nutriscore).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:            sodium_100g   R-squared:                       0.020
Model:                            OLS   Adj. R-squared:                  0.020
Method:                 Least Squares   F-statistic:                     1056.
Date:                Fri, 13 Jan 2023   Prob (F-statistic):               0.00
Time:                        18:28:12   Log-Likelihood:            -3.7647e+05
No. Observations:              203405   AIC:                         7.530e+05
Df Residuals:                  203400   BIC:                         7.530e+05
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   0.1305      0.008     15.383      0.000       0.114       0.147
nutrition_grade_fr[T.b]     0.0767      0.012      6.317      0.000       0.053       0.101
nutrition_grade_fr[T.c]     0.5417      0.011     47.820      0.000       0.519       0.564
nutrition_grade_fr[T.d]     0.5174      0.011     48.651      0.000       0.497       0.538
nutrition_grade_fr[T.e]     0.4620      0.012     40.169      0.000       0.439       0.485
==============================================================================
Omnibus:                   390895.401   Durbin-Watson:                   1.250
Prob(Omnibus):                  0.000   Jarque-Bera (JB):        753156329.427
Skew:                          15.237   Prob(JB):                         0.00
Kurtosis:                     299.542   Cond. No.                         6.46
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • R² et R² ajusted : SCE/SCT, sont inférieurs à 5% : 0.020 : la variable explicative nutrition_grade_fr n'est pas pertinentes pour expliquer proteins_100g
  • Omnibus : Une valeur proche de zero indique que les données suivent une loi Normal
    • Prob(Obminbus) : Confirme que les données ne suivent pas une loi normale (proche de 1) mais dans notre ici 0.000
  • Caractéristiques de forme
    • Coefficient d'applatissement Kurtosis : 15.2

L'analyse de variance unidirectionnelle est également connue sous le nom d'ANOVA à un seul facteur ou ANOVA simple. Comme son nom l'indique, ANOVA unidirectionnelle convient aux expériences comportant une seule variable indépendante (facteur) à deux niveaux ou plus. Par exemple, une variable dépendante peut être le mois de l'année où il y a le plus de fleurs dans le jardin. Il y aura douze niveaux. ANOVA unidirectionnelle présuppose :


  • Conditions :
    • Indépendance : la valeur de la variable dépendante pour une observation est indépendante de la valeur de toute autre observation.
    • Normalité : la valeur de la variable dépendante est normalement distribuée.
    • Variance : la variance est comparable dans les différents groupes expérimentaux.
    • Continue : la variable dépendante (nombre de fleurs) est continue et peut être mesurée sur une échelle qui peut être subdivisée.

ANALYSE MULTIVARIEE¶

Regression linéaire multiple¶

Quelles éléments expliquent le mieux le nutrigrade ?

In [69]:
df_test.columns
Out[69]:
Index(['code', 'creator', 'created_datetime', 'last_modified_datetime',
       'product_name', 'brands', 'categories_fr', 'countries_fr',
       'additives_n', 'additives_fr', 'ingredients_from_palm_oil_n',
       'nutrition_grade_fr', 'main_category_fr', 'energy_100g', 'fat_100g',
       'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
       'proteins_100g', 'salt_100g', 'sodium_100g', 'nutrition_score_fr_100g',
       'nutrition_grade_fr_le'],
      dtype='object')
  • COnditions de la regression linéaire
  • L'objectif est d'utiliser l'agorithme backward pour connaitre le nombre de variables nécéssaires pour déterminer le nutrigrade.

Ainsi on verra quellles variables sont déterminantes.

In [70]:
def backward_selected(data, response):
    """Linear model designed by backward selection.

    Parameters:
    -----------
    data : pandas DataFrame with all possible predictors and response

    response: string, name of response column in data

    Returns:
    --------
    model: an "optimal" fitted statsmodels linear model
           with an intercept
           selected by backward selection
           evaluated by parameters p-value
    """
    remaining = set(data._get_numeric_data().columns)
    if response in remaining:
        remaining.remove(response)
    cond = True

    while remaining and cond:
        formula = "{} ~ {} + 1".format(response,' + '.join(remaining))
        print('_______________________________')
        print(formula)
        model = smf.ols(formula, data).fit()
        score = model.pvalues[1:]
        toRemove = score[score == score.max()]
        if toRemove.values > 0.05:
            print('remove', toRemove.index[0], '(p-value :', round(toRemove.values[0],3), ')')
            remaining.remove(toRemove.index[0])
        else:
            cond = False
            print('is the final model !')
        print('')
    print(model.summary())
    
    return model
In [71]:
# Intanciation de la régression linéaire multiple
colonnes = ['nutrition_grade_fr_le','nutrition_score_fr_100g','main_category_fr', 'energy_100g', 'fat_100g',
       'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
       'proteins_100g', 'sodium_100g','salt_100g']
reg_backward = backward_selected(df_test[colonnes], 'nutrition_grade_fr_le')
_______________________________
nutrition_grade_fr_le ~ fat_100g + carbohydrates_100g + sodium_100g + sugars_100g + proteins_100g + fiber_100g + nutrition_score_fr_100g + salt_100g + energy_100g + saturated_fat_100g + 1
remove salt_100g (p-value : 0.209 )

_______________________________
nutrition_grade_fr_le ~ fat_100g + carbohydrates_100g + sodium_100g + sugars_100g + proteins_100g + fiber_100g + nutrition_score_fr_100g + energy_100g + saturated_fat_100g + 1
is the final model !

                              OLS Regression Results                             
=================================================================================
Dep. Variable:     nutrition_grade_fr_le   R-squared:                       0.920
Model:                               OLS   Adj. R-squared:                  0.920
Method:                    Least Squares   F-statistic:                 2.589e+05
Date:                   Fri, 13 Jan 2023   Prob (F-statistic):               0.00
Time:                           15:40:29   Log-Likelihood:                -93132.
No. Observations:                 203405   AIC:                         1.863e+05
Df Residuals:                     203395   BIC:                         1.864e+05
Df Model:                              9                                         
Covariance Type:               nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   0.9686      0.002    548.712      0.000       0.965       0.972
fat_100g                    0.0005      0.000      4.674      0.000       0.000       0.001
carbohydrates_100g         -0.0008   5.85e-05    -13.802      0.000      -0.001      -0.001
sodium_100g                -0.0182      0.001    -31.608      0.000      -0.019      -0.017
sugars_100g                -0.0021   6.64e-05    -31.534      0.000      -0.002      -0.002
proteins_100g              -0.0020      0.000    -15.832      0.000      -0.002      -0.002
fiber_100g                  0.0026      0.000     10.933      0.000       0.002       0.003
nutrition_score_fr_100g     0.1549      0.000    895.651      0.000       0.155       0.155
energy_100g             -5.347e-05   3.08e-06    -17.341      0.000   -5.95e-05   -4.74e-05
saturated_fat_100g         -0.0121      0.000    -69.869      0.000      -0.012      -0.012
==============================================================================
Omnibus:                     1146.143   Durbin-Watson:                   1.527
Prob(Omnibus):                  0.000   Jarque-Bera (JB):             1613.150
Skew:                          -0.056   Prob(JB):                         0.00
Kurtosis:                       3.421   Cond. No.                     2.92e+03
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 2.92e+03. This might indicate that there are
strong multicollinearity or other numerical problems.
  • Toutes les variables sont nécéssaires à par le sel d'après cette algorithme.

  • On regarde maintenant qu'elle est l'importance de l'énérgie et du nutrition_score

In [72]:
# On cherche l'élément qui explique le mieux le nutrigrade
anova_nutrigrade = smf.ols('energy_100g~nutrition_score_fr_100g', data=df_test).fit()
print(anova_nutrigrade.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:            energy_100g   R-squared:                       0.400
Model:                            OLS   Adj. R-squared:                  0.400
Method:                 Least Squares   F-statistic:                 1.356e+05
Date:                Fri, 13 Jan 2023   Prob (F-statistic):               0.00
Time:                        15:40:29   Log-Likelihood:            -1.5846e+06
No. Observations:              203405   AIC:                         3.169e+06
Df Residuals:                  203403   BIC:                         3.169e+06
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                 694.5013      1.842    377.061      0.000     690.891     698.111
nutrition_score_fr_100g    52.7403      0.143    368.227      0.000      52.460      53.021
==============================================================================
Omnibus:                    14505.600   Durbin-Watson:                   0.896
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            17898.063
Skew:                           0.692   Prob(JB):                         0.00
Kurtosis:                       3.445   Cond. No.                         18.3
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
In [73]:
# On cherche l'élément qui explique le mieux le nutrigrade
anova_nutrigrade = smf.ols('nutrition_grade_fr_le~nutrition_score_fr_100g', data=df_test).fit()
print(anova_nutrigrade.summary())
                              OLS Regression Results                             
=================================================================================
Dep. Variable:     nutrition_grade_fr_le   R-squared:                       0.916
Model:                               OLS   Adj. R-squared:                  0.916
Method:                    Least Squares   F-statistic:                 2.211e+06
Date:                   Fri, 13 Jan 2023   Prob (F-statistic):               0.00
Time:                           15:40:29   Log-Likelihood:                -98032.
No. Observations:                 203405   AIC:                         1.961e+05
Df Residuals:                     203403   BIC:                         1.961e+05
Df Model:                              1                                         
Covariance Type:               nonrobust                                         
===========================================================================================
                              coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------------
Intercept                   0.8888      0.001    720.419      0.000       0.886       0.891
nutrition_score_fr_100g     0.1427   9.59e-05   1487.052      0.000       0.142       0.143
==============================================================================
Omnibus:                     2100.768   Durbin-Watson:                   1.490
Prob(Omnibus):                  0.000   Jarque-Bera (JB):             3539.915
Skew:                           0.043   Prob(JB):                         0.00
Kurtosis:                       3.641   Cond. No.                         18.3
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
  • C'est essentiellement le nutrtion score qui explique l'appartenance au nutrigrade qui se calcule avec les variables nutritives et l'energy_100g est trés important d'après le R²

  • On regarde maintenant via l'ACP quelles sont les variables les mieux représentées et quelles sont leur relations

Analyse en Composantes Principales (ACP)¶

In [74]:
df_acp = df_nutriscore.copy()
In [75]:
df_acp.columns
Out[75]:
Index(['code', 'creator', 'created_datetime', 'last_modified_datetime',
       'product_name', 'brands', 'categories_fr', 'countries_fr',
       'additives_n', 'additives_fr', 'ingredients_from_palm_oil_n',
       'nutrition_grade_fr', 'main_category_fr', 'energy_100g', 'fat_100g',
       'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g', 'fiber_100g',
       'proteins_100g', 'salt_100g', 'sodium_100g', 'nutrition_score_fr_100g'],
      dtype='object')
In [76]:
# On isole les variables de notre ACP
cols_acp = ['energy_100g','fat_100g', 'saturated_fat_100g', 'carbohydrates_100g', 'sugars_100g',
           'fiber_100g', 'proteins_100g', 'salt_100g', 'sodium_100g','nutrition_score_fr_100g']
In [77]:
#Centrage et réduction
X = df_acp[cols_acp]
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

#Instanciation de l'ACP
pca = PCA(svd_solver='full').fit(X_scaled)
X_projected = pca.transform(X_scaled)

Eboulis des valeurs propres Afin d'avoir un aperçu du nombre de composantes nécessaire à l'analyse, nous allons projeter l'éboulis des valeurs propres :

In [78]:
#Variances expliquées
varexpl = pca.explained_variance_ratio_*100

#Projection de l'éboulis des valeurs propres
taux_var_exp = pca.explained_variance_ratio_
scree = taux_var_exp * 100
plt.bar(np.arange(len(scree)) + 1, scree, color='SteelBlue')
ax1 = plt.gca()
ax2 = ax1.twinx()
ax2.plot(np.arange(len(scree)) + 1, scree.cumsum(), c='red', marker='o')
ax2.set_ylabel('Taux cumulatif de l\'inertie')
ax1.set_xlabel('Rang de l\'axe d\'inertie')
ax1.set_ylabel('Pourcentage d\'inertie')
for i, p in enumerate(ax1.patches):
    ax1.text(p.get_width() /
             5 +
             p.get_x(),
             p.get_height() +
             p.get_y() +
             0.3,
             '{:.0f}%'.format(taux_var_exp[i] *100),
             fontsize=8,color='k')
plt.title("Eboulis des valeurs propres", fontdict=font_title)
plt.gcf().set_size_inches(8, 4)
plt.grid(False)
plt.show(block=False)
In [79]:
print("Le premier plan factoriel couvre une inertie de {:.2f}% et le second plan : {:.2f}%.".format(varexpl[0:2].sum(),
                                                                                                     varexpl[0:4].sum()))
Le premier plan factoriel couvre une inertie de 52.53% et le second plan : 83.15%.

Les 2 premiers plans factoriels couvrent une inertie d'un peu plus de 83,15%. Une analyse sur F1 et F2 semble donc cohérente.

Projection sur le cercle des corrélations

In [80]:
#Espace des composantes principales
pcs = pca.components_

#Matrice des corrélations variables x facteurs
p = X.shape[1]
sqrt_valprop = np.sqrt(pca.explained_variance_)
corvar = np.zeros((p, p))
for dim in range(p):
    corvar[:,dim] = pcs[dim,:] * sqrt_valprop[dim]

#on affiche pour les deux premiers plans factoriels 
corr_matrix = pd.DataFrame({'feature':X.columns,'CORR_F1':corvar[:,0],'CORR_F2':corvar[:,1], 
              'CORR_F3':corvar[:,2], 'CORR_F4':corvar[:,3]})
corr_matrix
Out[80]:
feature CORR_F1 CORR_F2 CORR_F3 CORR_F4
0 energy_100g 0.919577 -0.055475 0.003847 0.212015
1 fat_100g 0.777394 0.143944 -0.411599 -0.055548
2 saturated_fat_100g 0.752975 0.107812 -0.317509 -0.285934
3 carbohydrates_100g 0.413601 -0.334804 0.665704 0.353578
4 sugars_100g 0.436849 -0.368174 0.683280 -0.136313
5 fiber_100g 0.182201 -0.068756 -0.036149 0.861720
6 proteins_100g 0.252664 0.264638 -0.510740 0.429292
7 salt_100g 0.001630 0.926159 0.369268 0.057356
8 sodium_100g 0.001721 0.926165 0.369262 0.057372
9 nutrition_score_fr_100g 0.830060 0.120236 0.139848 -0.347973
In [81]:
#Variable Illustrative
ivNutrigrade = df_acp['nutrition_grade_fr'].values

#Encodage des grades pour l'acp
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
ivNutrigrade = encoder.fit_transform(ivNutrigrade)
ivNutrigrade = ivNutrigrade.reshape((ivNutrigrade.shape[0],1))

#Corrélation de la variable illustrative avec les axes factoriels 
corrIv = np.zeros((ivNutrigrade.shape[1],p))
for j in range(p): 
    for k in range(ivNutrigrade.shape[1]): 
        corrIv[k,j] = np.corrcoef(ivNutrigrade[:,k],X_projected[:,j])[0,1]
In [82]:
def cerle_corr(pcs, n_comp, pca, axis_ranks, 
               labels=None, label_rotation=0, 
               illustrative_var_label=None, illustrative_var_corr=None):
    for d1, d2 in axis_ranks:
        if d2 < n_comp:
            
            # initialisation de la figure
            fig=plt.figure(figsize=(10,10))
            fig.subplots_adjust(left=0.1,right=0.9,bottom=0.1,top=0.9)
            ax=fig.add_subplot(111)
            ax.set_aspect('equal', adjustable='box') 
            ax.set_facecolor("ivory")

            #détermination des limites du graphique
            ax.set_xlim(-1,1) 
            ax.set_ylim(-1,1) 

            #affichage des flèches 
            plt.quiver(np.zeros(pcs.shape[1]), np.zeros(pcs.shape[1]),
                       pcs[d1,:],pcs[d2,:], 
                       angles='xy', scale_units='xy', scale=1, 
                       color="black", alpha=0.5)
            # et noms de variables
            for i,(x,y) in enumerate(pcs[[d1,d2]].T):
                plt.annotate(labels[i],(x,y),
                             ha='center', va='center',
                             fontsize='14',color="black", alpha=0.8) 

            #variable illustrative
            if illustrative_var_label is not None :
                plt.annotate(illustrative_var_label,
                             (illustrative_var_corr[0,d1],illustrative_var_corr[0,d2]),
                             color='b')
                plt.quiver(np.zeros(pcs.shape[1]), np.zeros(pcs.shape[1]),
                                   illustrative_var_corr[0,d1],illustrative_var_corr[0,d2], 
                                   angles='xy', scale_units='xy', scale=1, color="b", alpha=0.5)

            #ajouter les axes 
            plt.plot([-1,1],[0,0],linewidth=1, color='black', ls='--') 
            plt.plot([0,0],[-1,1],linewidth=1, color='black', ls='--')

            #ajouter un cercle 
            cercle = plt.Circle((0,0),1,color='steelblue',fill=False) 
            ax.add_artist(cercle) 

            # nom des axes, avec le pourcentage d'inertie expliqué
            plt.xlabel('F{} ({}%)'.format(d1+1, round(100*pca.explained_variance_ratio_[d1],1)))
            plt.ylabel('F{} ({}%)'.format(d2+1, round(100*pca.explained_variance_ratio_[d2],1)))

            plt.title("Cercle des corrélations (F{} et F{})".format(d1+1, d2+1), fontdict=font_title)
            plt.show(block=False)
In [83]:
cerle_corr(pcs, 4, pca, [(0,1),(2,3)], labels = np.array(X.columns), 
           illustrative_var_label="Nutriscore_grade", illustrative_var_corr = corrIv)

Bilan


F1,F2

  • parties :
    • haut : Produits salés
    • bas : Produit plutôt sucré
    • gauche : peu calorique
    • droite : produit à forte teneur en énergie
  • Par raport au Nutrigrade
    • très corrélés aux produits gras, sucré et au nutrition_score_fr
  • Observations
    • Nutrition_score : très corrélés à : fat, , saturated_fat et nutriscore
    • carbohydrate et sugar
    • sodium et salt

F3, F4

  • parties :
    • haut :
    • bas : Produits gras et nutriscore élevé
    • gauche :
    • droite :
  • Par raport au Nutrigrade
    • Très corrélé au nutrition_score_fr (logique) est dans le bas du graphique (produits putôt gras, sucré et energie
    • Protéine est anticorrelé à l'axe
  • Observations
    • Les produits carbohydrate son anticorele au fat, saturated_fat
    • nutriscore et sugar au protéine (F4)
    • sodium, salt au fat
    • proteins au nutriscore

Projection des produits sur les plans factoriels

  • Visualiser la projection des individus sur ces premiers plans factoriels et donc en 2D :
In [84]:
def plot_plans_factoriels_nutrigrade(X_projected, n_comp, pca, axis_ranks, labels=None, alpha=1, illustrative_var=None):
    for d1,d2 in axis_ranks:
        if d2 < n_comp:
            # initialisation de la figure       
            fig = plt.figure(figsize=(12,8))
        
            # affichage des points
            if illustrative_var is None:
                plt.scatter(X_projected[:, d1], X_projected[:, d2], alpha=alpha)
            else:
                illustrative_var = np.array(illustrative_var)
                label_patches = []
                colors = ['#038141', '#85bb2f', '#fecb02', '#ee8100', '#e63e11']
                i = 0
                for value in np.unique(illustrative_var):
                    selected = np.where(illustrative_var == value)
                    plt.scatter(X_projected[selected, d1], X_projected[selected, d2], alpha=alpha, label=value, c=colors[i])
                    label_patch = mpatches.Patch(color=colors[i],
                                                 label=value)
                    label_patches.append(label_patch)
                    i += 1
                    plt.legend(
                        handles=label_patches,
                        bbox_to_anchor=(1.05,1),loc=2,borderaxespad=0.,facecolor='white')
               # plt.legend()
                

            # affichage des labels des points
            if labels is not None:
                for i,(x,y) in enumerate(X_projected[:,[d1,d2]]):
                    plt.text(x, y, labels[i],
                              fontsize='14', ha='center',va='center') 
                    
            
            # détermination des limites du graphique
            boundary = np.max(np.abs(X_projected[:, [d1,d2]]))*1.1
            plt.xlim([-boundary,boundary])
            plt.ylim([-boundary,boundary])
        
            # affichage des lignes horizontales et verticales
            plt.plot([-100, 100], [0, 0], color='grey', ls='--')
            plt.plot([0, 0], [-100, 100], color='grey', ls='--')

            # nom des axes, avec le pourcentage d'inertie expliqué
            plt.xlabel('F{} ({}%)'.format(d1+1, round(100*pca.explained_variance_ratio_[d1],1)))
            plt.ylabel('F{} ({}%)'.format(d2+1, round(100*pca.explained_variance_ratio_[d2],1)))

            plt.title("Projection des {} individus sur F{} et F{}".format(X_projected.shape[0], d1+1, d2+1), fontdict=font_title)
            plt.show(block=False)
In [85]:
plot_plans_factoriels_nutrigrade(X_projected, 4, pca, [(0,1),(2,3)], illustrative_var = df_nutriscore['nutrition_grade_fr'])
In [ ]:
 
In [86]:
def plot_plans_factoriels(X_projected, n_comp, pca, axis_ranks, labels=None, alpha=1, illustrative_var=None):
    for d1,d2 in axis_ranks:
        if d2 < n_comp:
            # initialisation de la figure       
            fig = plt.figure(figsize=(12,8))
        
            # affichage des points
            if illustrative_var is None:
                plt.scatter(X_projected[:, d1], X_projected[:, d2], alpha=alpha)
            else:
                illustrative_var = np.array(illustrative_var)
                for value in np.unique(illustrative_var):
                    selected = np.where(illustrative_var == value)
                    plt.scatter(X_projected[selected, d1], X_projected[selected, d2], alpha=alpha, label=value)
                plt.legend()
                

            # affichage des labels des points
            if labels is not None:
                for i,(x,y) in enumerate(X_projected[:,[d1,d2]]):
                    plt.text(x, y, labels[i],
                              fontsize='14', ha='center',va='center') 
                    
            
            # détermination des limites du graphique
            boundary = np.max(np.abs(X_projected[:, [d1,d2]]))*1.1
            plt.xlim([-boundary,boundary])
            plt.ylim([-boundary,boundary])
        
            # affichage des lignes horizontales et verticales
            plt.plot([-100, 100], [0, 0], color='grey', ls='--')
            plt.plot([0, 0], [-100, 100], color='grey', ls='--')

            # nom des axes, avec le pourcentage d'inertie expliqué
            plt.xlabel('F{} ({}%)'.format(d1+1, round(100*pca.explained_variance_ratio_[d1],1)))
            plt.ylabel('F{} ({}%)'.format(d2+1, round(100*pca.explained_variance_ratio_[d2],1)))

            plt.title("Projection des {} individus sur F{} et F{}".format(X_projected.shape[0], d1+1, d2+1), fontdict=font_title)
            plt.show(block=False)
In [87]:
plot_plans_factoriels(X_projected, 4, pca, [(0,1),(2,3)], illustrative_var = df_nutriscore['nutrition_grade_fr'])

Clustering¶

On tente le clustering : des clusters ptrotein, sodium et sel ?

In [88]:
# Copy et préparation des données
df_cluster_all = df_nutriscore.copy()
df_cluster = df_nutriscore[cols_quant_cont]
In [89]:
# Scaler
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(df_cluster)
df_cluster_scaled = scaler.transform(df_cluster)
In [90]:
# Méthode du coude pour trouver le nombre de clusters
from sklearn.cluster import KMeans
from yellowbrick.cluster import KElbowVisualizer

# Instantiate 
model = KMeans()
visualizer = KElbowVisualizer(model, k=(1, 11))

# Fit the data to the visualizer
visualizer.fit(df_cluster_scaled)    
plt.grid(False)
visualizer.show()
Out[90]:
<AxesSubplot: title={'center': 'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
In [91]:
# Avec k=3 le meilleur hyper-paramètre pour KMeans
from sklearn.cluster import KMeans

k_means = KMeans(n_clusters=3)
kmeans = k_means.fit(scaler.transform(df_cluster))
df_cluster_all['cluster'] = kmeans.labels_
df_cluster_all.head()
Out[91]:
code creator created_datetime last_modified_datetime product_name brands categories_fr countries_fr additives_n additives_fr ingredients_from_palm_oil_n nutrition_grade_fr main_category_fr energy_100g fat_100g saturated_fat_100g carbohydrates_100g sugars_100g fiber_100g proteins_100g salt_100g sodium_100g nutrition_score_fr_100g cluster
0 0000000004530 usda-ndb-import 2017-03-09 14:32:37 2017-03-09 14:32:37 Banana Chips Sweetened (Whole) inconnue inconnu États-Unis 0.0 0.0 d inconnu 2243.0 28.57 28.57 64.29 14.29 3.6 3.57 0.00000 0.000 14.0 1
1 0000000004559 usda-ndb-import 2017-03-09 14:32:37 2017-03-09 14:32:37 Peanuts Torn & Glasser inconnu États-Unis 0.0 0.0 b inconnu 1941.0 17.86 0.00 60.71 17.86 7.1 17.86 0.63500 0.250 0.0 1
2 0000000016087 usda-ndb-import 2017-03-09 10:35:31 2017-03-09 10:35:31 Organic Salted Nut Mix Grizzlies inconnu États-Unis 0.0 0.0 d inconnu 2540.0 57.14 5.36 17.86 3.57 7.1 17.86 1.22428 0.482 12.0 2
6 0000000016124 usda-ndb-import 2017-03-09 10:35:11 2017-03-09 10:35:12 Organic Muesli Daddy's Muesli inconnu États-Unis 2.0 E123 - Amarante,E307a - Tocophérol 0.0 c inconnu 1833.0 18.75 4.69 57.81 15.62 9.4 14.06 0.13970 0.055 7.0 1
10 0000000016872 usda-ndb-import 2017-03-09 10:34:10 2017-03-09 10:34:11 Zen Party Mix Sunridge inconnu États-Unis 1.0 E100 - Curcumine 0.0 d inconnu 2230.0 36.67 5.00 36.67 3.33 6.7 16.67 1.60782 0.633 12.0 2
In [92]:
plt.figure(figsize=(7, 7))
plt.title("Les Clusters", size=16,weight='bold')
nb_par_var = df_cluster_all['cluster'].sort_values().value_counts()
nb_par_var = nb_par_var.loc[sorted(nb_par_var.index)]
explode = [0.1]
for i in range(len(nb_par_var) - 1):
    explode.append(0)
wedges, texts, autotexts = plt.pie(nb_par_var, labels=nb_par_var.index, autopct='%1.1f%%', 
                                   colors=['b', 'darkblue', 'steelblue'], textprops={
                                       'fontsize': 16, 'color': 'black', 'backgroundcolor': 'w'},
                                   explode=explode)
axes = plt.gca()
axes.legend(wedges,nb_par_var.index,loc='center right',fontsize=14,
            bbox_to_anchor=(1,0,0.5, 1))
plt.show()

On a 3 cluster

In [93]:
plt.figure(figsize=(15, 8))
sns.countplot(x='cluster', hue='nutrition_grade_fr', data=df_cluster_all,
              palette=colors_nutri)

plt.legend(loc=1)
plt.ylabel('Nombre de produits', labelpad=20, fontsize=14)
plt.xticks(fontsize=14)
plt.title('Répartition des groupes de clusters par groupes Nutrigrade', fontsize=16)
plt.grid(False)
plt.show()

Ce n'est pas les nutrigrades

In [94]:
# Groupe 0
# Condition
mask0 = df_cluster_all['cluster'] == 0
# création d'un dataframe du cluster 0
df0 = df_cluster_all[mask0]['energy_100g']
# On sauvegarde le describe de ce df dans une variable
serie_0 = df0.describe()

# Groupe 1
mask1 = df_cluster_all['cluster'] == 1
df1 = df_cluster_all[mask1]['energy_100g']
serie_1 = df1.describe()
# Groupe 2
mask2 = df_cluster_all['cluster'] == 2
df2 = df_cluster_all[mask2]['energy_100g']
serie_2 = df2.describe()
In [95]:
# df_cluster_all des statistiques par groupe
# On crée une liste contenant nos 3 describe
liste_cluster = [serie_0.values, serie_1.values, serie_2.values]
# index
cols_cluster = serie_1.index
# Création df_cluster_all pour comparer les describe de nos 3 df de cluster
df_cluster_all_energy = pd.DataFrame(liste_cluster, columns=cols_cluster, index=['Groupe 0', 'Groupe 1', 'Groupe 2'])
# On affiche le résultat des groupes
df_cluster_all_energy
Out[95]:
count mean std min 25% 50% 75% max
Groupe 0 98218.0 535.007854 374.108366 0.0 222.0 448.0 815.0 2134.0
Groupe 1 75141.0 1682.905476 385.920792 0.0 1464.0 1655.0 1971.0 3736.0
Groupe 2 30046.0 2004.213482 598.852161 0.0 1494.0 1950.0 2389.0 3776.0

C'est l'énergy_100g

BILAN¶


Le jeu de données contient toutes les informations dont nous avons besoin pour apporter une information complémentaire de qualité au personne qui souhaite surveiller leur alimentation en situation de surveillance rénale.


  • Le poids : alimentation saine, équilibrée et plaisir : Nutrigrade
    • 'nutrition-score-fr_100g','energy_100g'
    • 'fat_100g', 'saturated-fat_100g','carbohydrates_100g', 'sugars_100g', 'fiber_100g'
  • Les indicateurs nécessaires :
    • Sel('salt_100g') et le Sodium('sodium_100g') : sel minéral
    • Proteines ('proteins_100g') : Au stade d’insuffisance rénale les besoins en protéines sont de 0,8g/Kg/j
      • en fonction du poids : l'application demandera le poid et le sexe de la personne pour avoir une indication personnalisée
    • Le phosphore est absent mais le phosphore est surtout présent dans les aliments, lié aux protéines
      • La limitation protéique entraîne déjà une diminution des apports en phosphore.
    • Limiter les additifs : Les additif cible sont présent dans le jeu de données